<?php
/**
 * This file defines the class {@see \BF\IO\Folder}
 *
 * @author     Kado <ulfikado@gmail.com>
 * @category   Beluga PHP5.3+ Framework
 * @package    BF\IO
 * @since      2015-01-31 00:29
 * @subpackage Core
 * @version    0.1
 */

namespace BF\IO
{

   /**
    * A static folder (directory) helping class.
    *
    * @since      v0.1
    */
   class Folder
   {

      # <editor-fold defaultstate="collapsed" desc=" - - >   P R I V A T E   C O N S T R U C T O R   - - - - - - - - - - - - - - - - -">

      /**
       * Inits a new instance.
       */
      private function __construct() { }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc=" - - -   P U B L I C   S T A T I C   M E T H O D S   - - - - - - - - - - - - - - -">

      /**
       * Goes the count of $upLevel up inside the defined folder. Each level up goes 1 level up to the parent folder.
       *
       * e.g.:
       *
       * <code>\BF\IO\Folder::Up( '/is/var/www', 2, '' );   // results in: '/is'</code>
       *
       * @param  string $folder  The base folder
       * @param  int    $upLevel How many folder levels we should go up? (Defaults to 1)
       * @param  string $endchar Append this character to the end of the resulting folder path. (Defaults to '')
       * @return string
       */
      public static function Up( $folder, $upLevel = 1, $endchar = '' )
      {
          for ( ; $upLevel > 0; --$upLevel )
          {
             $folder = \dirname( $folder );
          }
          return $folder . $endchar;
      }

      /**
       * Checks if the defined folder exists. If so, $folder is returned normalized. If not so,
       * the first existing parent folder is returned normalized.
       *
       * @param  string $folder
       * @return string
       */
      public static function GetFirstExisting( $folder )
      {
         $folder = \rtrim( $folder, '\\/' );
         if ( \BF\str_contains( $folder, '/' ) && \BF\str_contains( $folder, '\\' ) )
         {
            // Use mixed directory separators, normalize it
            if ( \DIRECTORY_SEPARATOR == '\\' )
            {
               $folder = \str_replace( '/', '\\', $folder );
            }
            else
            {
               $folder = \str_replace( '\\', '/', $folder );
            }
         }
         $level =
            \count(
               \explode(
                  \DIRECTORY_SEPARATOR,
                  \ltrim( $folder, '\\/' )
               )
            );
         for ( $i = 0; $i < $level && ! \is_dir( $folder ); ++$i )
         {
             $f = \dirname( $folder );
             if ( empty( $f ) || $f == $folder )
             {
                return '/';
             }
             $folder = $f;
         }
         \clearstatcache();
         return $folder;
      }

      /**
       * Checks, if the folder can be created. !Attention! If the foulder already exists FALSE is returned!
       *
       * @param  string $folder_path The folder to check.
       * @return boolean
       */
      public static function CanCreate( $folder_path )
      {
         if ( \is_dir( $folder_path ) )
         {
            return false;
         }
         $fip = self::GetFirstExisting( $folder_path );
         return \is_writable( $fip );
      }

      /**
       * Creates a new folder with defined mode. The mode only works on unixoid systems.
       *
       * @param  string  $folder The folder to create.
       * @param  integer $mode Mode for new folder (0700) only used for unixode systems
       * @throws \BF\IO\Exception If creation fails
       */
      public static function Create( $folder, $mode = 0700 )
      {
         if ( \is_dir( $folder ) )
         {
            return;
         }
         if ( ! self::CanCreate( $folder ) )
         {
            throw new \BF\IO\Exception(
               $folder,
               'Creation of folder fails cause basefolder isnt writable!'
            );
         }
         try
         {
            if ( \DIRECTORY_SEPARATOR == '\\' )
            {
               $res = \mkdir( $folder, null, true );
            }
            else
            {
               $res = \mkdir( $folder, $mode, true );
            }
         }
         catch ( \Exception $ex )
         {
            throw new \BF\IO\Exception(
               $folder,
               'Folder creation fails!',
               \E_USER_ERROR,
               $ex
            );
         }
         if ( ! $res || ! \is_dir( $folder ) )
         {
            throw new \BF\IO\Exception(
               $folder,
               'Unknown error while executing folder creation.'
            );
         }
         \clearstatcache();
      }

      /**
       * Deletes the defined folder recursive with all contained files and sub folders.
       *
       * @param  string  $folder The folder to delete.
       * @param  boolean $clear  Clear only all folder contents and dont Delete the main folder? (Defaults to FALSE)
       * @throws \BF\IO\Exception
       */
      public static function Delete( $folder, $clear = false )
      {
         if ( empty( $folder ) && ! \is_dir( $folder ) )
         {
            return;
         }
         $folder = \rtrim( $folder, '\\/' ) . \DIRECTORY_SEPARATOR;
         $openDir = \opendir( $folder );
         // ignore . and .. (its alway the first and second
         \readdir( $openDir ); \readdir( $openDir );
         while ( false !== ( $item = \readdir( $openDir ) ) )
         {
            $path = $folder . $item;
            if ( !\is_dir( $path ) )
            {
               \BF\IO\File::Delete( $path );
            }
            else
            {
               self::Delete( $path );
            }
         }
         if ( ! $clear )
         {
            return;
         }
         \closedir( $openDir );
         try { \rmdir( $folder ); }
         catch ( \Exception $ex )
         {
            throw new \BF\IO\Exception(
               $folder,
               'Could not delete the defined folder.',
               \E_USER_ERROR,
               $ex
            );
         }
      }

      /**
       * Returns all file paths inside the defined folder.
        *
        * @param  string  $folder
        * @param  boolean $recursive Also include subfolders? (Defaults to FALSE)
        * @return array
        */
      public static function ListAllFiles( $folder, $recursive = false )
      {
         $res = array();
         if ( \is_dir( $folder ) )
         {
            return $res;
         }
         if ( ! $recursive )
         {
            $d = \dir( $folder );
            $d->read(); $d->read();
            while ( false !== ( $entry = $d->read() ) )
            {
               $tmp = \BF\IO\Path::Combine( $folder, $entry );
               if ( ! \is_file( $tmp ) )
               {
                  continue;
               }
               $res[] = $tmp;
            }
            $d->close();
            return $res;
          }
          self::_listRecursive( $res, $folder );
          return $res;
      }

      /**
       * Returns all files, matching the definied filter.
       *
       * If $recursive is TRUE, it also includes al sub folders.
       *
       * If the filter is a callback function/method, so it must accept 2 parameters.
       *
       * - string $itemName The name of the current filter item
       * - string $itemPath THe absolute path of the item.
       *
       * @param  string          $folder    The folder
       * @param  string|callback $filter    Regex or callback|callable for filtering files
       * @param  boolean         $recursive Include subfolders? (Defaults to FALSE)
       * @return array
       */
      public static function ListFilteredFiles( $folder, $filter, $recursive = false )
      {
         $files = array();
         if ( ! \is_dir( $folder ) )
         {
            return $files;
         }
         if ( ! $recursive )
         {
            $d = \dir( $folder );
            while ( false !== ( $entry = $d->read() ) )
            {
               if ( $entry == '.' || $entry == '..' )
               {
                  continue;
               }
               $tmp = \BF\IO\Path::Combine( $folder, $entry );
               if ( ! \is_file( $tmp ) )
               {
                  continue;
               }
               if ( \is_callable( $filter ) )
               {
                  if ( \call_user_func( $filter, $entry, $tmp ) )
                  {
                     $files[] = $tmp;
                  }
                  continue;
               }
               try
               {
                  if ( ! \preg_match( $filter, $entry ) )
                  {
                     continue;
                  }
                  $files[] = $tmp;
               }
               catch ( \Exception $ex ) { $ex = null; }
            }
            $d->close();
            return $files;
         }
         self::_listRecursiveFiltered( $files, $filter, $folder );
         return $files;
      }

      /**
       * Returns the real path of the defined folder. If the folder is defined as a absolute path, its returned as it,
       * only normalized. Otherwise the $basepath is used to make the folder absolute.
       *
       * Its no checked, if the folder or path existsa!
       *
       * Examples:
       *
       * <code>
       * echo "'" . \BF\IO\Folder::GetRealPath('../xyz', '/abc/def') . "'";
       * # outputs '/abc/xyz'
       * echo "'" . \BF\IO\Folder::GetRealPath('./xyz', '/abc/def') . "'";
       * # outputs '/abc/def/xyz'
       * echo "'" . \BF\IO\Folder::GetRealPath('/xyz', '/abc/def') . "'";
       * # outputs '/xyz'
       * echo "'" . \BF\IO\Folder::GetRealPath('C:/xyz', 'C:/abc/def') . "'";
       * # outputs 'C:/xyz'
       * echo "'" . \BF\IO\Folder::GetRealPath('../../xyz', '/abc/def') . "'";
       * # outputs '/xyz'
       * </code>
       *
       * @param  string $folder   The folder
       * @param  string $basepath The base path used if the $folder is relative. If its empty, getcwd() is used.
       * @return string
       */
      public static function GetRealPath( $folder, $basepath )
      {

         if ( \is_null( $basepath ) || \strlen( $basepath ) < 1 )
         {
            // If no base path is defined use path given by getcwd()
            $basepath = getcwd();
         }

         // Meassure the length of $folder
         $flen = \strlen( $folder );

         // Switch basepath to OS directory separator and remove trailing directory separators
         $basepath = \rtrim( \BF\IO\Path::Normalize( $basepath ), \DIRECTORY_SEPARATOR );

         if ( $flen < 1 )
         {
            // return the base path if the folder is empty.
            return $basepath;
         }

         // Switch folder to OS directory separator
         $folder = \BF\IO\Path::Normalize( $folder );

         if ( $folder[ 0 ] == '/' )
         {
            // $folder is a absolute unix path. return it
            return $folder;
         }

         if ( $flen < 2 )
         {
            if ( $folder == '.' )
            {
               // $folder is only a dot '.'
               return $basepath;
            }
            // $folder is a single character
            return $basepath . \DIRECTORY_SEPARATOR . \trim( $folder, \DIRECTORY_SEPARATOR );
         }

         if ( $flen == 2 )
         {
            if ( \IS_WIN && $folder[ 1 ] == ':' )
            {
               // If its only like C: in windows systems, append a \
               return $folder . \DIRECTORY_SEPARATOR;
            }
            if ( $folder[ 0 ] == '.' && $folder[ 1 ] == '.' )
            {
               return \dirname( $basepath );
            }
            // Return normally combined
            return $basepath . \DIRECTORY_SEPARATOR . \trim( $folder, \DIRECTORY_SEPARATOR );
         }

         if ( ( $folder[ 1 ] == ':' && $folder[ 2 ] == '/' )
           || ( $folder[ 1 ] == '\\' && $folder[ 2 ] == '\\' ) )
         {
            // Absolute windows folders. Return it.
            return $folder;
         }

         // @todo: The next lines does not handle some /../ inside the folder

         // Remove all leading ../ or ..\
         while ( \BF\str_startswith( $folder, '..' . \DIRECTORY_SEPARATOR ) )
         {
            $basepath = \dirname( $basepath );
            $folder   = \substr( $folder, 3 );
         }

         return $basepath . \DIRECTORY_SEPARATOR . \trim( $folder, '/' );

      }

      /**
       * Removes all contents from defined folder (empties it)
       *
       * @param  string $folder
       * @throws \BF\IO\Exception
       */
      public static function Clear( $folder )
      {
         self::Delete( $folder, true );
      }

      /**
       * Moves all folder path elements (files + subfolders) from $sourceFolder to $targetFolder.
       *
       * The target folder will be created if it dont exists.
       *
       * The source folder it empty after this action.
       *
       * @param  string $sourceFolder The source folder.
       * @param  string $targetFolder The target folder.
       * @param  int    $tFolderMode  The mode of the target folder if it must be created. (Defaults to 0700)
       * @param  bool   $clearTarget  Clear the target folder if it has some contents (Defaults to FALSE)
       * @throws \BF\IO\Exception
       * @uses   \BF\IO\Folder::Copy() Uses internally the Copy method and clears after it the $sourcefolder
       */
      public static function MoveContents( $sourceFolder, $targetFolder, $tFolderMode = 0700, $clearTarget = false )
      {
         self::Copy( $sourceFolder, $targetFolder, $tFolderMode, $clearTarget );
         self::Clear( $sourceFolder );
      }

      /**
       * Moves the $sourceFolder to the $targetFolder. $sourceFolde will be deleted.
       *
       * @param  string $sourceFolder The source folder.
       * @param  string $targetFolder The target folder.
       * @param  int    $tFolderMode  The mode of the target folder if it must be created. (Defaults to 0700)
       * @param  bool   $clearTarget  Clear the target folder if it has some contents (Defaults to FALSE)
       * @throws \BF\IO\Exception
       */
      public static function Move( $sourceFolder, $targetFolder, $tFolderMode = 0700, $clearTarget = false)
      {
         self::Copy( $sourceFolder, $targetFolder, $tFolderMode, $clearTarget );
         self::Delete( $sourceFolder );
      }

      /**
       * Copies all folder path elements (files + subfolders) from $sourceFolder to $targetFolder.
       *
       * @param  string $sourceFolder The source folder.
       * @param  string $targetFolder The target folder.
       * @param  int    $tFolderMode  The mode of the target folder if it must be created. (Defaults to 0700)
       * @param  bool   $clearTarget  Clear the target folder if it has some contents (Defaults to FALSE)
       * @throws \BF\IO\Exception
       */
      public static function Copy( $sourceFolder, $targetFolder, $tFolderMode = 0700, $clearTarget = false )
      {

         // Remove trailing directory seperators
         $sourceFolder = \rtrim( $sourceFolder, '\\/' );
         $targetFolder = \rtrim( $targetFolder, '\\/' );

         if ( !\is_dir( $sourceFolder ) )
         {
            // If the source folder not exists, stop here…
            throw new \BF\IO\Exception(
               $sourceFolder,
               'Can not copy folder contents to another folder if defined source folder dont exists!'
            );
         }

         if ( \is_dir( $targetFolder ) )
         {
            // Target exists
            if ( $clearTarget )
            {
               // Clear the target
               self::Clear( $targetFolder );
            }
         }
         else
         {
            // Target folder dont exists, create it
            self::Create( $targetFolder, $tFolderMode );
         }

         // Start reading the folder elements
         $openDir = \opendir( $sourceFolder );

         // Ignore '.' and '..' items
         \readdir( $openDir ); \readdir( $openDir );

         // loop all other items
         while ( false !== ( $item = \readdir( $openDir ) ) )
         {
            $spath = $sourceFolder . '/' . $item;
            $tpath = $targetFolder . '/' . $item;
            if ( ! \is_dir( $spath ) )
            {
               // Its a file, copy it
               \BF\IO\File::Copy( $spath, $tpath );
            }
            else
            {
               // Its a folder, copy it
               self::Copy( $spath, $tpath, $tFolderMode );
            }
         }

         // End reading the folder elements
         \closedir( $openDir );

      }

      /**
       * Zips all folder contents to defined ZIP archive.
       *
       * @param  string  $sourceFolder The folder to zip
       * @param  string  $zipFile      The ZIP archive destination file
       * @param  string  $zFolderName  If you will zip it inside a special folder inside the archive, name the folder here.
       * @param  boolean $overwrite    Overwrite the archive if it exists? (Defaults to TRUE)
       * @throws \BF\MissingExtensionException     If there is no PHP ZIP support
       * @throws \BF\IO\FileAlreadyExistsException If the ZIP exists, and overwriting is disabled.
       * @throws \BF\IO\Exception                  In all other error cases, while ZIP writing.
       */
      public static function Zip( $sourceFolder, $zipFile, $zFolderName = null, $overwrite = true )
      {
         $oldfile = null;
         $res     = null;
         if ( ! \class_exists( 'ZipArchive' ) )
         {
            throw new \BF\MissingExtensionException(
               'Zip',
               '\BF\IO\Folder::Zip() fails with no ZIP-Support.'
            );
         }
         if ( \file_exists( $zipFile ) )
         {
            if ( ! $overwrite )
            {
               throw new \BF\IO\FileAlreadyExistsException(
                  $zipFile,
                  'Overwriting the ZIP-file is not allowed by code.'
               );
            }
            $oldfile = $zipFile . '.old';
            \BF\IO\File::Move( $zipFile, $oldfile );
         }
         $zip = new \ZipArchive();
         if ( true !== ( $res = $zip->open( $zipFile, \ZipArchive::CREATE ) ) )
         {
             if ( ! empty( $oldfile ) )
             {
                \BF\IO\File::Move( $oldfile, $zipFile );
             }
             self::___handWriteError( $res, $zipFile );
         }
         if ( empty( $zFolderName ) )
         {
             $tmp = \preg_split( '~[\\\\/]~', \dirname( $zipFile ) );
             $zFolderName = $tmp[ \count( $tmp ) - 1 ];
         }
         $zip->addEmptyDir( $zFolderName );
         $openDir = \opendir( $sourceFolder );
         \readdir( $openDir ); \readdir( $openDir );
         while ( false !== ( $item = \readdir( $openDir ) ) )
         {
             $itemPath = $sourceFolder . '/' . $item;
             self::___zipItem( $item, $itemPath, $zip, $zFolderName );
         }
         \closedir( $openDir );
         $zip->close();
      }

      # </editor-fold>

      # <editor-fold defaultstate="collapsed" desc=" - - -   P R I V A T E   S T A T I C   M E T H O D S   - - - - - - - - - - - - - -">

      /**
       * @param  $res
       * @param  $zipFile
       * @throws \BF\IO\Exception
       */
      private static function ___handWriteError( $res, $zipFile )
      {
         throw new \BF\IO\Exception(
            $zipFile,
            'Zipfile could not be created cause '
               . \BF\IO\File::GetZipArchiveError( $res ) );
      }

      /**
       * @param             $item
       * @param string      $itemPath
       * @param \ZipArchive $zip
       * @param string      $zFolderName
       */
      private static function ___zipItem( $item, $itemPath, \ZipArchive $zip, $zFolderName )
      {
         if ( \is_dir( $itemPath ) )
         {
            $zFolderName .= '/' . $item;
            $zip->addEmptyDir( $zFolderName );
            $openDir = \opendir( $itemPath );
            while ( false !== ( $itm = \readdir( $openDir ) ) )
            {
               if ( $itm == '.' || $itm == '..' )
               {
                  continue;
               }
               $itmPath = $itemPath . '/' . $itm;
               self::___zipItem( $itm, $itmPath, $zip, $zFolderName );
            }
         }
         else
         {
            $zip->addFile( $itemPath, $zFolderName . '/' . $item );
         }
      }

      /**
       * @param $res
       * @param $folder
       */
      private static function _listRecursive( &$res, $folder )
      {
         $d = \dir( $folder );
         $d->read(); $d->read();
         while ( false !== ( $entry = $d->read() ) )
         {
            $tmp = \BF\IO\Path::Combine( $folder, $entry );
            if ( \is_dir( $tmp ) )
            {
               self::_listRecursive( $res, $tmp );
            }
            else
            {
               $res[] = $tmp;
            }
         }
         $d->close();
      }

      /**
       * @param $res
       * @param $filter
       * @param $folder
       */
      private static function _listRecursiveFiltered( &$res, $filter, $folder )
      {
         $d = \dir( $folder );
         $d->read(); $d->read();
         while ( false !== ( $entry = $d->read() ) )
         {
            $tmp = \BF\IO\Path::Combine( $folder, $entry );
            if ( ! \is_file( $tmp ) )
            {
               self::_listRecursiveFiltered( $res, $filter, $tmp );
               continue;
            }
            if ( \is_callable( $filter ) )
            {
               if ( \call_user_func( $filter, $entry, $tmp ) )
               {
                  $res[] = $tmp;
               }
               continue;
            }
            try
            {
               if ( ! \preg_match( $filter, $entry ) )
               {
                  continue;
               }
               $res[] = $tmp;
            }
            catch ( \Exception $ex ) { $ex = null; }
         }
         $d->close();
      }

      # </editor-fold>

   }

}

